"use client"; import { useEffect, useMemo, useRef, useState } from "react"; import Breadcrumb from "@/app/components/reuseableUI/breadcrumb"; import EmptyState from "@/app/components/reuseableUI/emptyState"; import { ProductCard } from "@/app/components/reuseableUI/productCard"; import ItemsPerPageSelectClient from "@/app/components/shop/ItemsPerPageSelectClient"; import { partsLogicClient, type PLSearchProduct } from "@/lib/client/partslogic"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; interface PaginationInfo { total: number; page: number; per_page: number; total_pages: number; } interface CategoryCacheData { categoryName: string; products: PLSearchProduct[]; pagination: PaginationInfo; } const categoryCache = new Map(); type ItemsPerPage = 10 | 20 | 50 | 100; interface CategoryPageClientProps { slug: string; initialProducts?: PLSearchProduct[] | null; initialPagination?: PaginationInfo | null; initialCategoryName?: string; } function parsePositiveInt(value: string | null, fallback: number) { const n = value ? Number.parseInt(value, 10) : Number.NaN; if (!Number.isFinite(n) || n <= 0) return fallback; return n; } export default function CategoryPageClient(props: CategoryPageClientProps) { const { slug, initialProducts, initialPagination, initialCategoryName } = props; const searchParams = useSearchParams(); const router = useRouter(); const pathname = usePathname(); const skipFirstFetchRef = useRef(false); // Use initial data if provided (from SSR) const hasInitialData = Array.isArray(initialProducts) && initialPagination != null; skipFirstFetchRef.current = skipFirstFetchRef.current || hasInitialData; const [itemsPerPage, setItemsPerPage] = useState( (initialPagination?.per_page as ItemsPerPage) || 10 ); const [currentPage, setCurrentPage] = useState( initialPagination?.page || 1 ); const [searchQuery, setSearchQuery] = useState(""); const [products, setProducts] = useState( initialProducts || [] ); const [loading, setLoading] = useState(!hasInitialData); const [categoryName, setCategoryName] = useState( initialCategoryName || "" ); const [pagination, setPagination] = useState( initialPagination || { total: 0, page: 1, per_page: 10, total_pages: 0, } ); const [isInitialized, setIsInitialized] = useState(false); const urlState = useMemo(() => { const page = parsePositiveInt(searchParams.get("page"), 1); const perPageRaw = parsePositiveInt(searchParams.get("per_page"), itemsPerPage); const perPage = ([10, 20, 50, 100] as number[]).includes(perPageRaw) ? (perPageRaw as ItemsPerPage) : itemsPerPage; const q = (searchParams.get("q") || "").trim(); return { page, perPage, q }; }, [itemsPerPage, searchParams]); const buildHref = useMemo(() => { return (page: number) => { const params = new URLSearchParams(); if (urlState.q) params.set("q", urlState.q); if (urlState.perPage !== 10) params.set("per_page", String(urlState.perPage)); if (page > 1) params.set("page", String(page)); const qs = params.toString(); return qs ? `${pathname}?${qs}` : pathname; }; }, [pathname, urlState.perPage, urlState.q]); const updateURL = (next: { page: number; perPage: number; q: string }) => { const params = new URLSearchParams(); if (next.q) params.set("q", next.q); if (next.perPage !== 10) params.set("per_page", String(next.perPage)); if (next.page > 1) params.set("page", String(next.page)); const qs = params.toString(); const nextUrl = qs ? `${pathname}?${qs}` : pathname; router.replace(nextUrl, { scroll: false }); }; useEffect(() => { // Keep state driven by the URL (SSR baseline). This makes pagination/filtering // work without JS and ensures the client does not "fight" the server HTML. if (!isInitialized) setIsInitialized(true); if (currentPage !== urlState.page) setCurrentPage(urlState.page); if (itemsPerPage !== urlState.perPage) setItemsPerPage(urlState.perPage); if (searchQuery !== urlState.q) setSearchQuery(urlState.q); }, [currentPage, isInitialized, itemsPerPage, searchQuery, urlState.page, urlState.perPage, urlState.q]); useEffect(() => { if (!isInitialized) return; if (skipFirstFetchRef.current) { skipFirstFetchRef.current = false; return; } const cacheKey = `${slug}_${currentPage}_${itemsPerPage}_${searchQuery}`; const fetchCategoryAndProducts = async () => { if (categoryCache.has(cacheKey)) { const cached = categoryCache.get(cacheKey)!; setCategoryName(cached.categoryName); setProducts(cached.products); setPagination(cached.pagination); setLoading(false); return; } setLoading(true); try { const name = slug .replace(/-/g, " ") .replace(/\b\w/g, (l) => l.toUpperCase()); const response = await partsLogicClient.searchProducts({ category_slug: slug, page: currentPage, per_page: itemsPerPage, q: searchQuery || undefined, }); categoryCache.set(cacheKey, { categoryName: name, products: response.products || [], pagination: response.pagination, }); setCategoryName(name); setProducts(response.products || []); setPagination(response.pagination); } catch (e) { console.error(e); } finally { setLoading(false); } }; fetchCategoryAndProducts(); }, [currentPage, isInitialized, itemsPerPage, searchQuery, slug]); const breadcrumbItems = [ { text: "HOME", link: "/" }, { text: "SHOP", link: "/products/all" }, { text: categoryName || slug }, ]; return (
{/* H1 is rendered server-side in `src/app/category/[slug]/page.tsx` for SEO. */}

{categoryName || slug}

{searchQuery && (
{pagination.total > 0 ? `Found ${pagination.total} result${ pagination.total === 1 ? "" : "s" } for "${searchQuery}" in ${categoryName || slug}` : `No results found for "${searchQuery}" in ${ categoryName || slug }`}
)} {!searchQuery && (
{pagination.total} product{pagination.total === 1 ? "" : "s"}{" "} in {categoryName || slug}
)}
{ e.preventDefault(); updateURL({ page: 1, perPage: itemsPerPage, q: searchQuery }); }} >
setSearchQuery(e.target.value)} placeholder="Search in this category" className="w-full rounded-md border border-[var(--color-secondary-200)] px-3 py-2 text-sm font-secondary" />
{ setItemsPerPage(v); updateURL({ page: 1, perPage: v, q: searchQuery }); }} />
{loading && (
)}
{products && products.length > 0 ? products .sort((a, b) => a.name.localeCompare(b.name)) .map((item) => ( item.price_min ? item.price_max - item.price_min : null } isFeatured={ item.collection_names?.includes("Best Sellers") || false } onSale={(item.price_max || 0) > (item.price_min || 0)} skus={item.skus || []} /> )) : !loading && ( )}
{!loading && pagination.total_pages > 1 && ( )}
); }